前面我們已經成功把 FastAPI 部署到 Kubernetes,現在整個系統還非常單純:
Client
↓
API
使用者送出 Request,Kubernetes 的 Service 會把流量送到 FastAPI Pod,FastAPI 處理完之後直接回傳 Response。
不過真正的後端系統通常不會只有一個 API。Application 往往還需要連線資料庫、快取服務、Message Queue,甚至其他 Microservice。
所以今天我們要跨出非常重要的一步:
讓兩個 Kubernetes Service 彼此溝通。
今天我們會加入 Redis,並讓 FastAPI 每收到一次 Request,就把 Redis 裡的 visits 數字加一。
最後整個架構會變成:
Client
↓
Service: api
↓
API Pod
↓
Service: redis
↓
Redis Pod
這看起來只是多了一個 Redis,但其實今天會碰到 Kubernetes 裡非常重要的觀念:
Service Discovery 與 Kubernetes DNS。
我們希望使用者呼叫 FastAPI:
GET /
第一次可能回傳:
{
"message": "Hello from Kubernetes",
"visits": 1
}
再呼叫一次:
{
"message": "Hello from Kubernetes",
"visits": 2
}
這個 visits 並不是存在 FastAPI 自己的記憶體裡,而是存在 Redis。
也就是說,每次 Request 發生時,FastAPI 都必須先連到另一個 Pod:
FastAPI Pod
↓
Redis Pod
但這裡馬上出現一個問題。
FastAPI 要怎麼知道 Redis Pod 在哪裡?
這就是今天最重要的地方。
FastAPI 原本沒有 Redis Client,所以第一步先安裝 Python 的 Redis 套件。
修改:
app/requirements.txt
加入:
redis
之後重新 Build Docker Image 時,Docker 就會把這個套件一起安裝進新的 API Image。
接著修改:
app/main.py
內容變成:
import os
import redis
from fastapi import FastAPI
app = FastAPI()
REDIS_HOST = os.getenv(
"REDIS_HOST",
"redis"
)
redis_client = redis.Redis(
host=REDIS_HOST,
port=6379,
decode_responses=True
)
@app.get("/")
def root():
visits = redis_client.incr("visits")
return {
"message": "Hello from Kubernetes",
"visits": visits
}
@app.get("/health/live")
def live():
return {
"status": "alive"
}
這段程式碼最重要的不是 incr(),而是這一段:
REDIS_HOST = os.getenv(
"REDIS_HOST",
"redis"
)
它的意思是:
如果環境變數裡有:
REDIS_HOST
就使用環境變數提供的值。
如果沒有設定,就預設使用:
redis
接著 Redis Client 使用:
redis.Redis(
host=REDIS_HOST,
port=6379
)
也就是 FastAPI 會嘗試連線:
redis:6379
看到這裡你可能會覺得奇怪。
redis 又不是 IP Address,電腦怎麼知道它在哪裡?
這就是 Kubernetes Service 發揮作用的地方。
假設 Redis Pod 現在的 IP 是:
10.244.0.12
我們當然可以讓 FastAPI 直接連:
10.244.0.12:6379
但這是一個非常不好的做法。
原因是 Pod 本身是暫時性的。
如果 Redis Pod Crash:
Redis Pod A
10.244.0.12
Kubernetes 重新建立一個新的 Pod,新的 Pod 很可能變成:
Redis Pod B
10.244.0.25
如果 FastAPI 寫死:
10.244.0.12
那新的 Redis Pod 建立之後,FastAPI 還是會繼續連舊 IP,Application 就會壞掉。
所以 Kubernetes 不希望 Application 直接依賴 Pod IP。
正確做法是:
FastAPI
↓
Redis Service
↓
Redis Pod
FastAPI 只需要知道 Service 的名稱:
redis
至於後面的 Redis Pod IP 是什麼,交給 Kubernetes 處理。
建立:
k8s/04-redis.yaml
內容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis # Deployment 名稱
namespace: cka-lab # 部署在 cka-lab 命名空間下
spec: # 【外層 spec】:Deployment 的運作規則
replicas: 1 # Pod 的期望副本數為 1。死掉會自動重生,維持有 1 個在跑
selector: # Deployment 用來認養 Pod 的條件
matchLabels:
app: redis
template: # 【Pod 模板】:所有被建立出來的 Pod 都長這樣
metadata:
labels: # 給每個產出的 Pod 貼上的標籤
app: redis
spec: # 【內層 spec】:Pod 裡面運行容器的具體設定
containers:
- name: redis # 容器名稱
image: redis:7-alpine # Docker 映像檔版本
ports:
- containerPort: 6379 # 容器內部監聽的 Port
---
apiVersion: v1
kind: Service
metadata:
name: redis # Service 名稱(同 namespace 內可直接透過 "redis" 連線)
namespace: cka-lab
spec: # 【Service 的 spec】
selector:
app: redis # 將流量導向所有帶有 app: redis 標籤的 Pod
ports:
- port: 6379 # Service 自己對外暴露的 Port
targetPort: 6379 # 轉發進後端 Pod 容器的目標 Port

這個 YAML 其實建立了兩個不同的 Kubernetes Resource:
Deployment
+
Service
Deployment 負責:
建立與管理 Redis Pod
Service 則負責:
提供一個穩定的存取入口
因此架構實際上是:
Redis Service
↓
Redis Pod
Service 裡:
selector:
app: redis
會去尋找具有:
labels:
app: redis
的 Pod。
而 Redis Deployment 建立的 Pod 剛好就有:
labels:
app: redis
因此 Service 就知道:
我要把流量送給哪些 Pod。
Redis Service 裡有:
ports:
- port: 6379
targetPort: 6379
可以簡單理解成:
Service Port
6379
↓
Pod Port
6379
port 是 Service 對外提供的 Port。
targetPort 則是 Service 最後要把流量送到 Pod 的哪一個 Port。
因為 Redis 本身預設就是監聽:
6379
所以這裡兩個數字剛好一樣。
因此 FastAPI:
redis:6379
實際流量就會變成:
FastAPI Pod
↓
redis:6379
↓
抵達 Redis Service
↓
Redis Pod:6379
redis 這個名稱可以直接使用?這是 Kubernetes 很重要的一個功能:
內建 DNS。
當我們建立:
kind: Service
metadata:
name: redis
namespace: cka-lab
Kubernetes 會讓 Cluster 內的 Pod 可以透過 Service Name 找到它。
因此同一個 Namespace 裡的 FastAPI 可以直接使用:
redis
Kubernetes DNS 會幫我們解析成 Redis Service。
完整的 DNS Name 實際上大致是:
redis.cka-lab.svc.cluster.local
它可以拆成:
redis
↓
Service Name
cka-lab
↓
Namespace
svc
↓
Service
cluster.local
↓
Cluster Domain
所以:
redis
其實只是同 Namespace 情況下的簡寫。
也就是說,FastAPI 根本不需要知道 Redis Pod IP。
只要知道:
Service Name = redis
就夠了。
這就是 Kubernetes 的 Service Discovery。
因為我們剛剛修改了:
requirements.txt
以及:
main.py
這些都屬於 Application 程式碼與 Dependency 的改動,所以原本的 Docker Image 已經不包含最新內容。
因此需要重新 Build。
建立:
docker build -t cka-api:v3 ./app
現在本機就會多出:
cka-api:v3
因為我們使用的是 kind,而 kind Node 本身其實跑在 Docker Container 裡,所以本機 Docker 裡的 Image 不代表 kind Cluster 一定看得到。
因此還需要:
kind load docker-image cka-api:v3 --name cka-lab
把 Image 放進 kind Cluster。

接著把 API Deployment 使用的 Image 修改成:
image: cka-api:v3
例如:
containers:
- name: api
image: cka-api:v3
最後重新套用 Kubernetes YAML:
kubectl apply -f k8s/
!
Kubernetes 會發現:
原本 API Deployment
cka-api:v2
已經變成:
cka-api:v3
Deployment 的 Pod Template 發生變化,因此 Kubernetes 會自動進行 Rolling Update,建立新的 Pod,逐步替換舊 Pod。
可以查看:
kubectl get pods -n cka-lab
也可以查看 Deployment:
kubectl rollout status \
deployment/api \
-n cka-lab
https://ithelp.ithome.com.tw/upload/images/20260910/20168537vSfInCY4ob.png
如果一切正常,新的 API Pod 就會開始使用:
cka-api:v3
9/10
接著把 API Service 暫時 Forward 到 Mac。
執行:
kubectl port-forward service/api 8080:80 -n cka-lab
現在:
Mac localhost:8080
會被轉送到:
API Service:80
另外開一個 Terminal:
curl localhost:8080
第一次可能看到:
{
"message": "Hello from Kubernetes",
"visits": 1
}
!
再執行一次:
curl localhost:8080
變成:
{
"message": "Hello from Kubernetes",
"visits": 2
}
https://ithelp.ithome.com.tw/upload/images/20260910/20168537ZV7szH8aSC.png
如果數字真的持續增加,就代表我們已經成功完成:
Mac
↓
API Service
↓
API Pod
↓
Redis Service
↓
Redis Pod
這跟前幾天只有:
Client
↓
API Service
↓
API Pod
相比已經往真正的 Microservice Architecture 跨出很大一步。
因為現在已經不是單一 Application 自己運作,而是:
一個 Service
需要依賴另一個 Service。
既然我們說:
redis
可以被 Kubernetes DNS 解析,那當然可以自己驗證看看。
首先可以進入 API Pod:
kubectl exec -it deployment/api -n cka-lab -- sh
進去之後嘗試:
getent hosts redis

如果這個 Container Image 有安裝對應工具,就可以看到 DNS 解析結果。
不過 Production Image 通常會盡量精簡,因此裡面不一定會放 nslookup、dig 這類除錯工具。
這時候更常見的做法,是另外建立一個暫時的 Debug Pod。
例如:
kubectl run dns-test \
-n cka-lab \
--image=busybox:1.36 \
--restart=Never \
--rm -it \
-- \
nslookup redis
這個指令會暫時建立:
dns-test Pod
並在裡面執行:
nslookup redis
你應該可以看到 Kubernetes 把 redis 解析到 Redis Service。
完整名稱會類似:
redis.cka-lab.svc.cluster.local
測試結束之後:
--rm
也會自動把這個 Pod 刪除。
這其實是一個非常實用的 Kubernetes Troubleshooting 技巧。
當 Application 說:
連不到某個 Service
第一件可以確認的事情之一,就是:
DNS 到底有沒有解析成功?
現在我們反過來故意把 Application 弄壞。
假設 API Deployment 原本會從 ConfigMap 取得:
REDIS_HOST
原本:
REDIS_HOST=redis
我們故意改成:
REDIS_HOST=not-redis
接著讓 API Pod 重新建立,使新的環境變數載入。
這時候 FastAPI 會嘗試連線:
not-redis:6379
但 Kubernetes 裡根本沒有叫:
not-redis
的 Service。
因此 Application 呼叫 Redis 時就會失敗。
但是這時候如果執行:
kubectl get pods -n cka-lab
你可能還是會看到:
api-xxxxx 1/1 Running
這裡出現了一個非常重要的 Kubernetes 觀念。
Kubernetes 顯示:
Running
主要代表:
Container Process 還活著。
例如 FastAPI Process 還在執行:
uvicorn
所以 Kubernetes 會認為:
Container 還沒有死。
但使用者實際呼叫:
GET /
FastAPI 又會嘗試:
連線 Redis
Redis Host 寫錯之後,Request 就可能直接失敗。
所以現在出現:
Pod = Running
但是:
Application = 無法正常服務
這兩件事情其實完全可以同時發生。
這也是 Kubernetes 初學者非常容易誤會的地方。
看到:
STATUS = Running
不代表你的服務真的沒有問題。
今天表面上是在:
安裝 Redis
但真正重要的其實是我們第一次建立了:
Application A
↓
Application B
之間的依賴關係。
FastAPI 不需要知道 Redis Pod 真正的 IP,只需要連:
redis:6379
其中:
redis
是 Kubernetes Service Name。
Kubernetes DNS 負責:
Service Name
↓
Service
而 Service 再透過 Selector 找到:
Pod
因此整個流量實際上是:
FastAPI
↓
DNS 查詢 redis
↓
Redis Service
↓
Service Selector
↓
Redis Pod
這就是 Kubernetes Service Discovery 最核心的概念。
而最後我們又發現另一個問題:
Pod Running
並不能保證:
Application Healthy
那 Kubernetes 到底要怎麼知道:
我的 FastAPI 真的還活著嗎?
以及:
它現在到底能不能接收流量?
這就會帶出下一篇非常重要的主題:
Liveness Probe
Readiness Probe
到了這裡,我們的 Kubernetes 專案也正式從:
「把 Container 跑起來」
開始進入:
「如何讓真正的服務穩定運作」
的階段。